{
    "cells": [
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "<i>Copyright (c) Recommenders contributors.</i>\n",
                "\n",
                "<i>Licensed under the MIT License.</i>"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "# Benchmark with Movielens dataset\n",
                "\n",
                "This illustrative comparison applies to collaborative filtering algorithms available in this repository such as Spark ALS, Surprise SVD, SAR and others using the Movielens dataset. These algorithms are usable in a variety of recommendation tasks, including product or news recommendations.\n",
                "\n",
                "The main purpose of this notebook is not to produce comprehensive benchmarking results on multiple datasets. Rather, it is intended to illustrate on how one could evaluate different recommender algorithms using tools in this repository.\n",
                "\n",
                "## Experimentation setup:\n",
                "\n",
                "* Objective\n",
                "  * To compare how each collaborative filtering algorithm perform in predicting ratings and recommending relevant items.\n",
                "\n",
                "* Environment\n",
                "  * The comparison is run on a machine with 4 CPUs, 30Gb of RAM, and 1 GPU GeForce GTX 1660 Ti with 6Gb of memory.\n",
                "  * It should be noted that a local machine is not supposed to run scalable benchmarking analysis. Either scaling up or out the computing instances is necessary to run the benchmarking in an run-time efficient way without any memory issue.\n",
                "  * **NOTE ABOUT THE DEPENDENCIES TO INSTALL**: This notebook uses CPU, GPU and PySpark algorithms, so make sure you install the `full environment` as detailed in the [SETUP.md](../../SETUP.md). \n",
                "  \n",
                "* Datasets\n",
                "  * [Movielens 100K](https://grouplens.org/datasets/movielens/100k/).\n",
                "  * [Movielens 1M](https://grouplens.org/datasets/movielens/1m/).\n",
                "\n",
                "* Data split\n",
                "  * The data is split into train and test sets.\n",
                "  * The split ratios are 75-25 for train and test datasets.\n",
                "  * The splitting is stratified based on items. \n",
                "\n",
                "* Model training\n",
                "  * A recommendation model is trained by using each of the collaborative filtering algorithms. \n",
                "  * Empirical parameter values reported [here](http://mymedialite.net/examples/datasets.html) are used in this notebook. More exhaustive hyper parameter tuning would be required to further optimize results.\n",
                "\n",
                "* Evaluation metrics\n",
                "  * Ranking metrics:\n",
                "    * Precision@k.\n",
                "    * Recall@k.\n",
                "    * Normalized discounted cumulative gain@k (NDCG@k).\n",
                "    * Mean-average-precision (MAP). \n",
                "    * In the evaluation metrics above, k = 10. \n",
                "  * Rating metrics:\n",
                "    * Root mean squared error (RMSE).\n",
                "    * Mean average error (MAE).\n",
                "    * R squared.\n",
                "    * Explained variance.\n",
                "  * Run time performance\n",
                "    * Elapsed for training a model and using a model for predicting/recommending k items. \n",
                "    * The time may vary across different machines. "
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Globals settings"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 1,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Remove warnings\n",
                "import warnings\n",
                "warnings.filterwarnings(\"ignore\")\n",
                "import os\n",
                "os.environ[\"PYTHONWARNINGS\"] = \"ignore\"\n",
                "os.environ[\"SPARK_LOCAL_IP\"] = \"127.0.0.1\"  # Set local IP to avoid hostname warnings\n",
                "import logging\n",
                "logging.basicConfig(level=logging.ERROR)\n",
                "logging.getLogger(\"py4j\").setLevel(logging.ERROR)\n",
                "logging.getLogger(\"pyspark\").setLevel(logging.ERROR)"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 2,
            "metadata": {},
            "outputs": [
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "System version: 3.11.9 | packaged by conda-forge | (main, Apr 19 2024, 18:36:13) [GCC 12.3.0]\n",
                        "Number of cores: 8\n",
                        "NumPy version: 1.26.4\n",
                        "Pandas version: 2.2.2\n",
                        "Cornac version: 2.3.0\n",
                        "Surprise version: 1.1.4\n",
                        "PySpark version: 3.5.1\n",
                        "CUDA version: 12.1\n",
                        "CuDNN version: 8902\n",
                        "TensorFlow version: 2.15.1\n",
                        "PyTorch version: 2.3.1+cu121\n"
                    ]
                }
            ],
            "source": [
                "import sys\n",
                "import numpy as np\n",
                "import pandas as pd\n",
                "import cornac\n",
                "\n",
                "try:\n",
                "    import pyspark\n",
                "    from recommenders.utils.spark_utils import start_or_get_spark\n",
                "except ImportError:\n",
                "    pass  # skip this import if we are not in a Spark environment\n",
                "\n",
                "try:\n",
                "    import tensorflow as tf # NOTE: TF needs to be imported before PyTorch, otherwise we get an error\n",
                "    tf.get_logger().setLevel(\"ERROR\") # only show error messages\n",
                "    import torch\n",
                "    from recommenders.utils.gpu_utils import get_cuda_version, get_cudnn_version\n",
                "except ImportError:\n",
                "    pass  # skip this import if we are not in a GPU environment\n",
                "\n",
                "try:\n",
                "    import surprise # Put SVD surprise back in core deps when #2224 is fixed\n",
                "except:\n",
                "    pass \n",
                "\n",
                "current_path = os.path.join(os.getcwd(), \"examples\", \"06_benchmarks\") # To execute the notebook programmatically from root folder\n",
                "sys.path.append(current_path)\n",
                "from benchmark_utils import * \n",
                "from recommenders.datasets import movielens\n",
                "from recommenders.utils.general_utils import get_number_processors\n",
                "from recommenders.datasets.python_splitters import python_stratified_split\n",
                "from recommenders.utils.notebook_utils import store_metadata\n",
                "\n",
                "\n",
                "print(f\"System version: {sys.version}\")\n",
                "print(f\"Number of cores: {get_number_processors()}\")\n",
                "print(f\"NumPy version: {np.__version__}\")\n",
                "print(f\"Pandas version: {pd.__version__}\")\n",
                "print(f\"Cornac version: {cornac.__version__}\")\n",
                "\n",
                "try:\n",
                "    print(f\"Surprise version: {surprise.__version__}\") # Put SVD surprise back in core deps when #2224 is fixed\n",
                "except NameError:\n",
                "    pass\n",
                "\n",
                "try:\n",
                "    print(f\"PySpark version: {pyspark.__version__}\")\n",
                "except NameError:\n",
                "    pass  # skip this import if we are not in a Spark environment\n",
                "\n",
                "try:\n",
                "    print(f\"CUDA version: {get_cuda_version()}\")\n",
                "    print(f\"CuDNN version: {get_cudnn_version()}\")\n",
                "    print(f\"TensorFlow version: {tf.__version__}\")\n",
                "    print(f\"PyTorch version: {torch.__version__}\")\n",
                "except NameError:\n",
                "    pass  # skip this import if we are not in a GPU environment\n",
                "\n",
                "%load_ext autoreload\n",
                "%autoreload 2"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 3,
            "metadata": {},
            "outputs": [
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "your 131072x1 screen size is bogus. expect trouble\n",
                        "Setting default log level to \"WARN\".\n",
                        "To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).\n",
                        "25/07/20 10:53:46 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n"
                    ]
                }
            ],
            "source": [
                "try:\n",
                "    spark = start_or_get_spark(\"PySpark\", memory=\"32g\")\n",
                "    spark.conf.set(\"spark.sql.analyzer.failAmbiguousSelfJoin\", \"false\")\n",
                "    # Suppress Spark warnings\n",
                "    spark.sparkContext.setLogLevel(\"ERROR\")    \n",
                "    log4j = spark._jvm.org.apache.log4j\n",
                "    log4j.LogManager.getLogger(\"org\").setLevel(log4j.Level.ERROR)\n",
                "    log4j.LogManager.getLogger(\"akka\").setLevel(log4j.Level.ERROR)\n",
                "    log4j.LogManager.getLogger(\"org.apache.spark\").setLevel(log4j.Level.ERROR)\n",
                "    log4j.LogManager.getLogger(\"org.spark_project\").setLevel(log4j.Level.ERROR)\n",
                "except NameError:\n",
                "    pass  # skip this import if we are not in a Spark environment"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 4,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Fix random seeds to make sure out runs are reproducible\n",
                "np.random.seed(SEED)\n",
                "try:\n",
                "    tf.random.set_seed(SEED)\n",
                "    torch.manual_seed(SEED)\n",
                "    torch.cuda.manual_seed_all(SEED)\n",
                "except NameError:\n",
                "    pass  # skip this import if we are not in a GPU environment"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Parameters"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 5,
            "metadata": {
                "tags": [
                    "parameters"
                ]
            },
            "outputs": [],
            "source": [
                "data_sizes = [\"100k\"] # Movielens data size: 100k, 1m, 10m, or 20m\n",
                "algorithms = [\"als\", \"svd\", \"sar\", \"ncf\", \"embdotbias\", \"bpr\", \"bivae\", \"lightgcn\"]\n"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 6,
            "metadata": {},
            "outputs": [],
            "source": [
                "environments = {\n",
                "    \"als\": \"pyspark\",\n",
                "    \"sar\": \"python_cpu\",\n",
                "    \"svd\": \"python_cpu\",\n",
                "    \"embdotbias\": \"python_gpu\",\n",
                "    \"ncf\": \"python_gpu\",\n",
                "    \"bpr\": \"python_cpu\",\n",
                "    \"bivae\": \"python_gpu\",\n",
                "    \"lightgcn\": \"python_gpu\",\n",
                "}\n",
                "\n",
                "metrics = {\n",
                "    \"als\": [\"rating\", \"ranking\"],\n",
                "    \"sar\": [\"ranking\"],\n",
                "    \"svd\": [\"rating\", \"ranking\"],\n",
                "    \"embdotbias\": [\"rating\", \"ranking\"],\n",
                "    \"ncf\": [\"ranking\"],\n",
                "    \"bpr\": [\"ranking\"],\n",
                "    \"bivae\": [\"ranking\"],\n",
                "    \"lightgcn\": [\"ranking\"]\n",
                "}"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "Algorithm parameters"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 7,
            "metadata": {},
            "outputs": [],
            "source": [
                "als_params = {\n",
                "    \"rank\": 10,\n",
                "    \"maxIter\": 20,\n",
                "    \"implicitPrefs\": False,\n",
                "    \"alpha\": 0.1,\n",
                "    \"regParam\": 0.05,\n",
                "    \"coldStartStrategy\": \"drop\",\n",
                "    \"nonnegative\": False,\n",
                "    \"userCol\": DEFAULT_USER_COL,\n",
                "    \"itemCol\": DEFAULT_ITEM_COL,\n",
                "    \"ratingCol\": DEFAULT_RATING_COL,\n",
                "}\n",
                "\n",
                "sar_params = {\n",
                "    \"similarity_type\": \"jaccard\",\n",
                "    \"time_decay_coefficient\": 30,\n",
                "    \"time_now\": None,\n",
                "    \"timedecay_formula\": True,\n",
                "    \"col_user\": DEFAULT_USER_COL,\n",
                "    \"col_item\": DEFAULT_ITEM_COL,\n",
                "    \"col_rating\": DEFAULT_RATING_COL,\n",
                "    \"col_timestamp\": DEFAULT_TIMESTAMP_COL,\n",
                "}\n",
                "\n",
                "svd_params = {\n",
                "    \"n_factors\": 150,\n",
                "    \"n_epochs\": 15,\n",
                "    \"lr_all\": 0.005,\n",
                "    \"reg_all\": 0.02,\n",
                "    \"random_state\": SEED,\n",
                "    \"verbose\": False\n",
                "}\n",
                "\n",
                "embdotbias_params = {\n",
                "    \"n_factors\": 40, \n",
                "    \"y_range\": [0,5.5], \n",
                "    \"wd\": 1e-1,\n",
                "    \"lr_max\": 5e-3,\n",
                "    \"epochs\": 15\n",
                "}\n",
                "\n",
                "ncf_params = {\n",
                "    \"model_type\": \"NeuMF\",\n",
                "    \"n_factors\": 4,\n",
                "    \"layer_sizes\": [16, 8, 4],\n",
                "    \"n_epochs\": 15,\n",
                "    \"batch_size\": 1024,\n",
                "    \"learning_rate\": 1e-3,\n",
                "    \"verbose\": 10\n",
                "}\n",
                "\n",
                "bpr_params = {\n",
                "    \"k\": 200,\n",
                "    \"max_iter\": 200,\n",
                "    \"learning_rate\": 0.01,\n",
                "    \"lambda_reg\": 1e-3,\n",
                "    \"seed\": SEED,\n",
                "    \"verbose\": False\n",
                "}\n",
                "\n",
                "bivae_params = {\n",
                "    \"k\": 100,\n",
                "    \"encoder_structure\": [200],\n",
                "    \"act_fn\": \"tanh\",\n",
                "    \"likelihood\": \"pois\",\n",
                "    \"n_epochs\": 500,\n",
                "    \"batch_size\": 1024,\n",
                "    \"learning_rate\": 0.001,\n",
                "    \"seed\": SEED,\n",
                "    \"use_gpu\": True,\n",
                "    \"verbose\": False\n",
                "}\n",
                "\n",
                "lightgcn_param = {\n",
                "    \"model_type\": \"lightgcn\",\n",
                "    \"n_layers\": 3,\n",
                "    \"batch_size\": 1024,\n",
                "    \"embed_size\": 64,\n",
                "    \"decay\": 0.0001,\n",
                "    \"epochs\": 20,\n",
                "    \"learning_rate\": 0.005,\n",
                "    \"eval_epoch\": 5,\n",
                "    \"top_k\": DEFAULT_K,\n",
                "    \"metrics\": [\"recall\", \"ndcg\", \"precision\", \"map\"],\n",
                "    \"save_model\":False,\n",
                "    \"MODEL_DIR\":\".\",\n",
                "}\n",
                "\n",
                "params = {\n",
                "    \"als\": als_params,\n",
                "    \"sar\": sar_params,\n",
                "    \"svd\": svd_params,\n",
                "    \"embdotbias\": embdotbias_params,\n",
                "    \"ncf\": ncf_params,\n",
                "    \"bpr\": bpr_params,\n",
                "    \"bivae\": bivae_params,\n",
                "    \"lightgcn\": lightgcn_param,\n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 8,
            "metadata": {},
            "outputs": [],
            "source": [
                "prepare_training_data = {\n",
                "    \"als\": prepare_training_als,\n",
                "    \"sar\": prepare_training_sar,\n",
                "    \"svd\": prepare_training_svd,\n",
                "    \"embdotbias\": prepare_training_embdotbias,\n",
                "    \"ncf\": prepare_training_ncf,\n",
                "    \"bpr\": prepare_training_cornac,\n",
                "    \"bivae\": prepare_training_cornac,\n",
                "    \"lightgcn\": prepare_training_lightgcn,\n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 9,
            "metadata": {},
            "outputs": [],
            "source": [
                "prepare_metrics_data = {\n",
                "    \"als\": lambda train, test: prepare_metrics_als(train, test),\n",
                "    \"embdotbias\": lambda train, test: prepare_metrics_embdotbias(train, test),    \n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 10,
            "metadata": {},
            "outputs": [],
            "source": [
                "trainer = {\n",
                "    \"als\": lambda params, data: train_als(params, data),\n",
                "    \"svd\": lambda params, data: train_svd(params, data),\n",
                "    \"sar\": lambda params, data: train_sar(params, data), \n",
                "    \"embdotbias\": lambda params, data: train_embdotbias(params, data),\n",
                "    \"ncf\": lambda params, data: train_ncf(params, data),\n",
                "    \"bpr\": lambda params, data: train_bpr(params, data),\n",
                "    \"bivae\": lambda params, data: train_bivae(params, data),\n",
                "    \"lightgcn\": lambda params, data: train_lightgcn(params, data),\n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 11,
            "metadata": {},
            "outputs": [],
            "source": [
                "rating_predictor = {\n",
                "    \"als\": lambda model, test: predict_als(model, test),\n",
                "    \"svd\": lambda model, test: predict_svd(model, test),\n",
                "    \"embdotbias\": lambda model, test: predict_embdotbias(model, test),\n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 12,
            "metadata": {},
            "outputs": [],
            "source": [
                "ranking_predictor = {\n",
                "    \"als\": lambda model, test, train: recommend_k_als(model, test, train),\n",
                "    \"sar\": lambda model, test, train: recommend_k_sar(model, test, train),\n",
                "    \"svd\": lambda model, test, train: recommend_k_svd(model, test, train),\n",
                "    \"embdotbias\": lambda model, test, train: recommend_k_embdotbias(model, test, train),\n",
                "    \"ncf\": lambda model, test, train: recommend_k_ncf(model, test, train),\n",
                "    \"bpr\": lambda model, test, train: recommend_k_bpr(model, test, train),\n",
                "    \"bivae\": lambda model, test, train: recommend_k_bivae(model, test, train),\n",
                "    \"lightgcn\": lambda model, test, train: recommend_k_lightgcn(model, test, train),\n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 13,
            "metadata": {},
            "outputs": [],
            "source": [
                "rating_evaluator = {\n",
                "    \"als\": lambda test, predictions: rating_metrics_pyspark(test, predictions),\n",
                "    \"svd\": lambda test, predictions: rating_metrics_python(test, predictions),\n",
                "    \"embdotbias\": lambda test, predictions: rating_metrics_python(test, predictions)\n",
                "}\n",
                "    \n",
                "    \n",
                "ranking_evaluator = {\n",
                "    \"als\": lambda test, predictions, k: ranking_metrics_pyspark(test, predictions, k),\n",
                "    \"sar\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "    \"svd\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "    \"embdotbias\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "    \"ncf\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "    \"bpr\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "    \"bivae\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "    \"lightgcn\": lambda test, predictions, k: ranking_metrics_python(test, predictions, k),\n",
                "}"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 14,
            "metadata": {},
            "outputs": [],
            "source": [
                "def generate_summary(data, algo, k, train_time, time_rating, rating_metrics, time_ranking, ranking_metrics):\n",
                "    summary = {\"Data\": data, \"Algo\": algo, \"K\": k, \"Train time (s)\": train_time, \"Predicting time (s)\": time_rating, \"Recommending time (s)\": time_ranking}\n",
                "    if rating_metrics is None:\n",
                "        rating_metrics = {\n",
                "            \"RMSE\": np.nan,\n",
                "            \"MAE\": np.nan,\n",
                "            \"R2\": np.nan,\n",
                "            \"Explained Variance\": np.nan,\n",
                "        }\n",
                "    if ranking_metrics is None:\n",
                "        ranking_metrics = {\n",
                "            \"MAP\": np.nan,\n",
                "            \"nDCG@k\": np.nan,\n",
                "            \"Precision@k\": np.nan,\n",
                "            \"Recall@k\": np.nan,\n",
                "        }\n",
                "    summary.update(rating_metrics)\n",
                "    summary.update(ranking_metrics)\n",
                "    return summary"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Benchmark loop"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 15,
            "metadata": {
                "scrolled": true
            },
            "outputs": [
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "100%|██████████| 4.81k/4.81k [00:01<00:00, 3.60kKB/s]\n"
                    ]
                },
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "Size of Movielens 100k: (100000, 4)\n",
                        "\n",
                        "Computing als algorithm on Movielens 100k\n"
                    ]
                },
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "                                                                                \r"
                    ]
                },
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "Training time: 12.3537s\n",
                        "Rating prediction time: 0.0996s\n"
                    ]
                },
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "                                                                                \r"
                    ]
                },
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "Ranking prediction time: 0.1375s\n"
                    ]
                },
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "                                                                                \r"
                    ]
                },
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "\n",
                        "Computing svd algorithm on Movielens 100k\n",
                        "Training time: 0.9307s\n",
                        "Rating prediction time: 0.1459s\n",
                        "Ranking prediction time: 13.2209s\n",
                        "\n",
                        "Computing sar algorithm on Movielens 100k\n",
                        "Training time: 0.2343s\n",
                        "Ranking prediction time: 0.0936s\n",
                        "\n",
                        "Computing ncf algorithm on Movielens 100k\n",
                        "Training time: 113.2512s\n",
                        "Ranking prediction time: 8.9240s\n",
                        "\n",
                        "Computing embdotbias algorithm on Movielens 100k\n",
                        "Training time: 81.8275s\n",
                        "Rating prediction time: 0.0344s\n",
                        "Ranking prediction time: 1.6463s\n",
                        "\n",
                        "Computing bpr algorithm on Movielens 100k\n",
                        "Training time: 4.9720s\n",
                        "Ranking prediction time: 0.5015s\n",
                        "\n",
                        "Computing bivae algorithm on Movielens 100k\n",
                        "Training time: 22.6604s\n",
                        "Ranking prediction time: 0.6609s\n",
                        "\n",
                        "Computing lightgcn algorithm on Movielens 100k\n",
                        "Already create adjacency matrix.\n",
                        "Already normalize adjacency matrix.\n",
                        "Using xavier initialization.\n",
                        "Epoch 1 (train)1.5s: train loss = 0.47301 = (mf)0.47277 + (embed)0.00024\n",
                        "Epoch 2 (train)1.2s: train loss = 0.29106 = (mf)0.29044 + (embed)0.00063\n",
                        "Epoch 3 (train)1.2s: train loss = 0.25669 = (mf)0.25591 + (embed)0.00078\n",
                        "Epoch 4 (train)1.0s: train loss = 0.23832 = (mf)0.23737 + (embed)0.00095\n",
                        "Epoch 5 (train)1.1s + (eval)0.2s: train loss = 0.23030 = (mf)0.22921 + (embed)0.00109, recall = 0.15839, ndcg = 0.34424, precision = 0.29608, map = 0.21449\n",
                        "Epoch 6 (train)1.1s: train loss = 0.22182 = (mf)0.22062 + (embed)0.00120\n",
                        "Epoch 7 (train)0.9s: train loss = 0.21442 = (mf)0.21311 + (embed)0.00131\n",
                        "Epoch 8 (train)1.2s: train loss = 0.20012 = (mf)0.19867 + (embed)0.00145\n",
                        "Epoch 9 (train)1.0s: train loss = 0.18799 = (mf)0.18638 + (embed)0.00161\n",
                        "Epoch 10 (train)1.1s + (eval)0.4s: train loss = 0.18314 = (mf)0.18136 + (embed)0.00178, recall = 0.17879, ndcg = 0.38731, precision = 0.33531, map = 0.25201\n",
                        "Epoch 11 (train)1.3s: train loss = 0.17189 = (mf)0.16996 + (embed)0.00192\n",
                        "Epoch 12 (train)0.9s: train loss = 0.17146 = (mf)0.16940 + (embed)0.00206\n",
                        "Epoch 13 (train)1.1s: train loss = 0.16832 = (mf)0.16614 + (embed)0.00217\n",
                        "Epoch 14 (train)1.0s: train loss = 0.16489 = (mf)0.16261 + (embed)0.00228\n",
                        "Epoch 15 (train)1.0s + (eval)0.3s: train loss = 0.16108 = (mf)0.15869 + (embed)0.00239, recall = 0.18933, ndcg = 0.40226, precision = 0.34698, map = 0.26235\n",
                        "Epoch 16 (train)1.2s: train loss = 0.16342 = (mf)0.16094 + (embed)0.00248\n",
                        "Epoch 17 (train)0.9s: train loss = 0.15913 = (mf)0.15657 + (embed)0.00256\n",
                        "Epoch 18 (train)1.1s: train loss = 0.15884 = (mf)0.15618 + (embed)0.00266\n",
                        "Epoch 19 (train)1.1s: train loss = 0.15295 = (mf)0.15020 + (embed)0.00276\n",
                        "Epoch 20 (train)1.0s + (eval)0.4s: train loss = 0.15085 = (mf)0.14799 + (embed)0.00286, recall = 0.19599, ndcg = 0.41637, precision = 0.35917, map = 0.27617\n",
                        "Training time: 23.1118s\n",
                        "Ranking prediction time: 0.0602s\n",
                        "\n",
                        "Computation finished\n",
                        "CPU times: user 5min 40s, sys: 1min 32s, total: 7min 13s\n",
                        "Wall time: 5min 59s\n"
                    ]
                }
            ],
            "source": [
                "%%time\n",
                "\n",
                "# For each data size and each algorithm, a recommender is evaluated. \n",
                "cols = [\"Data\", \"Algo\", \"K\", \"Train time (s)\", \"Predicting time (s)\", \"RMSE\", \"MAE\", \"R2\", \"Explained Variance\", \"Recommending time (s)\", \"MAP\", \"nDCG@k\", \"Precision@k\", \"Recall@k\"]\n",
                "df_results = pd.DataFrame(columns=cols)\n",
                "\n",
                "for data_size in data_sizes:\n",
                "    # Load the dataset\n",
                "    df = movielens.load_pandas_df(\n",
                "        size=data_size,\n",
                "        header=[DEFAULT_USER_COL, DEFAULT_ITEM_COL, DEFAULT_RATING_COL, DEFAULT_TIMESTAMP_COL]\n",
                "    )\n",
                "    print(\"Size of Movielens {}: {}\".format(data_size, df.shape))\n",
                "    \n",
                "    # Split the dataset\n",
                "    df_train, df_test = python_stratified_split(df,\n",
                "                                                ratio=0.75, \n",
                "                                                min_rating=1, \n",
                "                                                filter_by=\"item\", \n",
                "                                                col_user=DEFAULT_USER_COL, \n",
                "                                                col_item=DEFAULT_ITEM_COL\n",
                "                                                )\n",
                "   \n",
                "    # Loop through the algos\n",
                "    for algo in algorithms:\n",
                "        print(f\"\\nComputing {algo} algorithm on Movielens {data_size}\")\n",
                "          \n",
                "        # Data prep for training set\n",
                "        train = prepare_training_data.get(algo, lambda x,y:(x,y))(df_train, df_test)\n",
                "        \n",
                "        # Get model parameters\n",
                "        model_params = params[algo]\n",
                "          \n",
                "        # Train the model\n",
                "        model, time_train = trainer[algo](model_params, train)\n",
                "        print(f\"Training time: {time_train}s\")\n",
                "                \n",
                "        # Predict and evaluate\n",
                "        train, test = prepare_metrics_data.get(algo, lambda x,y:(x,y))(df_train, df_test)\n",
                "        \n",
                "        if \"rating\" in metrics[algo]:   \n",
                "            # Predict for rating\n",
                "            preds, time_rating = rating_predictor[algo](model, test)\n",
                "            print(f\"Rating prediction time: {time_rating}s\")\n",
                "            \n",
                "            # Evaluate for rating\n",
                "            ratings = rating_evaluator[algo](test, preds)\n",
                "        else:\n",
                "            ratings = None\n",
                "            time_rating = np.nan\n",
                "        \n",
                "        if \"ranking\" in metrics[algo]:\n",
                "            # Predict for ranking\n",
                "            top_k_scores, time_ranking = ranking_predictor[algo](model, test, train)\n",
                "            print(f\"Ranking prediction time: {time_ranking}s\")\n",
                "            \n",
                "            # Evaluate for rating\n",
                "            rankings = ranking_evaluator[algo](test, top_k_scores, DEFAULT_K)\n",
                "        else:\n",
                "            rankings = None\n",
                "            time_ranking = np.nan\n",
                "            \n",
                "        # Record results\n",
                "        summary = generate_summary(data_size, algo, DEFAULT_K, time_train, time_rating, ratings, time_ranking, rankings)\n",
                "        df_results.loc[df_results.shape[0] + 1] = summary\n",
                "        \n",
                "print(\"\\nComputation finished\")\n"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Results"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 16,
            "metadata": {},
            "outputs": [
                {
                    "data": {
                        "text/html": [
                            "<div>\n",
                            "<style scoped>\n",
                            "    .dataframe tbody tr th:only-of-type {\n",
                            "        vertical-align: middle;\n",
                            "    }\n",
                            "\n",
                            "    .dataframe tbody tr th {\n",
                            "        vertical-align: top;\n",
                            "    }\n",
                            "\n",
                            "    .dataframe thead th {\n",
                            "        text-align: right;\n",
                            "    }\n",
                            "</style>\n",
                            "<table border=\"1\" class=\"dataframe\">\n",
                            "  <thead>\n",
                            "    <tr style=\"text-align: right;\">\n",
                            "      <th></th>\n",
                            "      <th>Data</th>\n",
                            "      <th>Algo</th>\n",
                            "      <th>K</th>\n",
                            "      <th>Train time (s)</th>\n",
                            "      <th>Predicting time (s)</th>\n",
                            "      <th>RMSE</th>\n",
                            "      <th>MAE</th>\n",
                            "      <th>R2</th>\n",
                            "      <th>Explained Variance</th>\n",
                            "      <th>Recommending time (s)</th>\n",
                            "      <th>MAP</th>\n",
                            "      <th>nDCG@k</th>\n",
                            "      <th>Precision@k</th>\n",
                            "      <th>Recall@k</th>\n",
                            "    </tr>\n",
                            "  </thead>\n",
                            "  <tbody>\n",
                            "    <tr>\n",
                            "      <th>1</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>als</td>\n",
                            "      <td>10</td>\n",
                            "      <td>12.3537</td>\n",
                            "      <td>0.0996</td>\n",
                            "      <td>0.966511</td>\n",
                            "      <td>0.753765</td>\n",
                            "      <td>0.268321</td>\n",
                            "      <td>0.263871</td>\n",
                            "      <td>0.1375</td>\n",
                            "      <td>0.003276</td>\n",
                            "      <td>0.033186</td>\n",
                            "      <td>0.038494</td>\n",
                            "      <td>0.013406</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>2</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>svd</td>\n",
                            "      <td>10</td>\n",
                            "      <td>0.9307</td>\n",
                            "      <td>0.1459</td>\n",
                            "      <td>0.942506</td>\n",
                            "      <td>0.744752</td>\n",
                            "      <td>0.299984</td>\n",
                            "      <td>0.299985</td>\n",
                            "      <td>13.2209</td>\n",
                            "      <td>0.012060</td>\n",
                            "      <td>0.094445</td>\n",
                            "      <td>0.089077</td>\n",
                            "      <td>0.030094</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>3</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>sar</td>\n",
                            "      <td>10</td>\n",
                            "      <td>0.2343</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>0.0936</td>\n",
                            "      <td>0.114038</td>\n",
                            "      <td>0.393819</td>\n",
                            "      <td>0.340615</td>\n",
                            "      <td>0.185377</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>4</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>ncf</td>\n",
                            "      <td>10</td>\n",
                            "      <td>113.2512</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>8.9240</td>\n",
                            "      <td>0.095231</td>\n",
                            "      <td>0.368634</td>\n",
                            "      <td>0.326935</td>\n",
                            "      <td>0.163221</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>5</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>embdotbias</td>\n",
                            "      <td>10</td>\n",
                            "      <td>81.8275</td>\n",
                            "      <td>0.0344</td>\n",
                            "      <td>0.992760</td>\n",
                            "      <td>0.776040</td>\n",
                            "      <td>0.223344</td>\n",
                            "      <td>0.223393</td>\n",
                            "      <td>1.6463</td>\n",
                            "      <td>0.018954</td>\n",
                            "      <td>0.117810</td>\n",
                            "      <td>0.104242</td>\n",
                            "      <td>0.042450</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>6</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>bpr</td>\n",
                            "      <td>10</td>\n",
                            "      <td>4.9720</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>0.5015</td>\n",
                            "      <td>0.133977</td>\n",
                            "      <td>0.444991</td>\n",
                            "      <td>0.388653</td>\n",
                            "      <td>0.216555</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>7</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>bivae</td>\n",
                            "      <td>10</td>\n",
                            "      <td>22.6604</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>0.6609</td>\n",
                            "      <td>0.143562</td>\n",
                            "      <td>0.468673</td>\n",
                            "      <td>0.408165</td>\n",
                            "      <td>0.220898</td>\n",
                            "    </tr>\n",
                            "    <tr>\n",
                            "      <th>8</th>\n",
                            "      <td>100k</td>\n",
                            "      <td>lightgcn</td>\n",
                            "      <td>10</td>\n",
                            "      <td>23.1118</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>NaN</td>\n",
                            "      <td>0.0602</td>\n",
                            "      <td>0.120902</td>\n",
                            "      <td>0.416371</td>\n",
                            "      <td>0.359173</td>\n",
                            "      <td>0.195985</td>\n",
                            "    </tr>\n",
                            "  </tbody>\n",
                            "</table>\n",
                            "</div>"
                        ],
                        "text/plain": [
                            "   Data        Algo   K Train time (s) Predicting time (s)      RMSE  \\\n",
                            "1  100k         als  10        12.3537              0.0996  0.966511   \n",
                            "2  100k         svd  10         0.9307              0.1459  0.942506   \n",
                            "3  100k         sar  10         0.2343                 NaN       NaN   \n",
                            "4  100k         ncf  10       113.2512                 NaN       NaN   \n",
                            "5  100k  embdotbias  10        81.8275              0.0344  0.992760   \n",
                            "6  100k         bpr  10         4.9720                 NaN       NaN   \n",
                            "7  100k       bivae  10        22.6604                 NaN       NaN   \n",
                            "8  100k    lightgcn  10        23.1118                 NaN       NaN   \n",
                            "\n",
                            "        MAE        R2  Explained Variance Recommending time (s)       MAP  \\\n",
                            "1  0.753765  0.268321            0.263871                0.1375  0.003276   \n",
                            "2  0.744752  0.299984            0.299985               13.2209  0.012060   \n",
                            "3       NaN       NaN                 NaN                0.0936  0.114038   \n",
                            "4       NaN       NaN                 NaN                8.9240  0.095231   \n",
                            "5  0.776040  0.223344            0.223393                1.6463  0.018954   \n",
                            "6       NaN       NaN                 NaN                0.5015  0.133977   \n",
                            "7       NaN       NaN                 NaN                0.6609  0.143562   \n",
                            "8       NaN       NaN                 NaN                0.0602  0.120902   \n",
                            "\n",
                            "     nDCG@k  Precision@k  Recall@k  \n",
                            "1  0.033186     0.038494  0.013406  \n",
                            "2  0.094445     0.089077  0.030094  \n",
                            "3  0.393819     0.340615  0.185377  \n",
                            "4  0.368634     0.326935  0.163221  \n",
                            "5  0.117810     0.104242  0.042450  \n",
                            "6  0.444991     0.388653  0.216555  \n",
                            "7  0.468673     0.408165  0.220898  \n",
                            "8  0.416371     0.359173  0.195985  "
                        ]
                    },
                    "execution_count": 16,
                    "metadata": {},
                    "output_type": "execute_result"
                }
            ],
            "source": [
                "df_results"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 17,
            "metadata": {},
            "outputs": [
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.03318593061498845,
                            "encoder": "json",
                            "name": "als"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "als"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.0944448901872182,
                            "encoder": "json",
                            "name": "svd"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "svd"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.3938185694234946,
                            "encoder": "json",
                            "name": "sar"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "sar"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.36863449662264747,
                            "encoder": "json",
                            "name": "ncf"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "ncf"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.11781034870835909,
                            "encoder": "json",
                            "name": "embdotbias"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "embdotbias"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.44499071687886577,
                            "encoder": "json",
                            "name": "bpr"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "bpr"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.46867268304477894,
                            "encoder": "json",
                            "name": "bivae"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "bivae"
                        }
                    },
                    "output_type": "display_data"
                },
                {
                    "data": {
                        "application/notebook_utils.json+json": {
                            "data": 0.4163712063482657,
                            "encoder": "json",
                            "name": "lightgcn"
                        }
                    },
                    "metadata": {
                        "notebook_utils": {
                            "data": true,
                            "display": false,
                            "name": "lightgcn"
                        }
                    },
                    "output_type": "display_data"
                }
            ],
            "source": [
                "# Record results for tests - ignore this cell\n",
                "for algo in algorithms:\n",
                "    store_metadata(algo, df_results.loc[df_results[\"Algo\"] == algo, \"nDCG@k\"].values[0])\n"
            ]
        }
    ],
    "metadata": {
        "kernelspec": {
            "display_name": "recommenders311",
            "language": "python",
            "name": "python3"
        },
        "language_info": {
            "codemirror_mode": {
                "name": "ipython",
                "version": 3
            },
            "file_extension": ".py",
            "mimetype": "text/x-python",
            "name": "python",
            "nbconvert_exporter": "python",
            "pygments_lexer": "ipython3",
            "version": "3.11.9"
        }
    },
    "nbformat": 4,
    "nbformat_minor": 4
}
